/*
 * Copyright (C) 2001-2011 Jacek Sieka, arnetheduck on gmail point com
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "stdinc.h"
#include "UserConnection.h"

#include "ClientManager.h"
#include "ResourceManager.h"

#include "StringTokenizer.h"
#include "AdcCommand.h"
#include "Transfer.h"
#include "DebugManager.h"
#include "CViewlinkDBManager.h"
#include "FavoriteManager.h"
#include "Download.h"

const string UserConnection::FEATURE_MINISLOTS = "MiniSlots";
const string UserConnection::FEATURE_XML_BZLIST = "XmlBZList";
const string UserConnection::FEATURE_ADCGET = "ADCGet";
const string UserConnection::FEATURE_ZLIB_GET = "ZLIG";
const string UserConnection::FEATURE_TTHL = "TTHL";
const string UserConnection::FEATURE_TTHF = "TTHF";
const string UserConnection::FEATURE_ADC_BAS0 = "BAS0";
const string UserConnection::FEATURE_ADC_BASE = "BASE";
const string UserConnection::FEATURE_ADC_BZIP = "BZIP";
const string UserConnection::FEATURE_ADC_TIGR = "TIGR";
const string UserConnection::FEATURE_BANMSG = "BanMsg"; // !SMT!-B

const string UserConnection::FILE_NOT_AVAILABLE = "File Not Available";

const string UserConnection::UPLOAD = "Upload";
const string UserConnection::DOWNLOAD = "Download";

void UserConnection::on(BufferedSocketListener::Line, const string& aLine) noexcept
{

	if (aLine.length() < 2)
		return;
		
	COMMAND_DEBUG(aLine, DebugManager::CLIENT_IN, getRemoteIp());
	
	if (aLine[0] == 'C' && !isSet(FLAG_NMDC))
	{
		if (!Text::validateUtf8(aLine))
		{
			// @todo Report to user?
			return;
		}
		dispatch(aLine);
		return;
	}
	else if (aLine[0] == '$')
	{
		setFlag(FLAG_NMDC);
	}
	else
	{
		// We shouldn't be here?
		if (getUser() && aLine.length() < 255)
			ClientManager::getInstance()->setUnknownCommand(getUser(), aLine);

		fire(UserConnectionListener::ProtocolError(), this, "Invalid data");  // TODO: translate
		return;
	}

	string cmd;
	string param;
	
	string::size_type x;
	
	if ((x = aLine.find(' ')) == string::npos)
	{
		cmd = aLine.substr(1);
	}
	else
	{
		cmd = aLine.substr(1, x - 1);
		param = aLine.substr(x + 1);
	}
	
	if (cmd == "MyNick")
	{
		if (!param.empty())
			fire(UserConnectionListener::MyNick(), this, param);
	}
	else if (cmd == "Direction")
	{
		x = param.find(" ");
		if (x != string::npos)
		{
			fire(UserConnectionListener::Direction(), this, param.substr(0, x), param.substr(x + 1));
		}
	}
	else if (cmd == "Error")
	{
		if (stricmp(param.c_str(), FILE_NOT_AVAILABLE) == 0 ||
		        param.rfind(/*path/file*/" no more exists") != string::npos)
		{
			// [+] SSA
			if (getDownload()->isSet(Download::FLAG_USER_GET_IP))
			{
				fire(UserConnectionListener::CheckUserIP(), this);
			}
			else
				fire(UserConnectionListener::FileNotAvailable(), this);
		}
		else if (strnicmp(param.c_str(), "BAN ", 4) == 0)   // !SMT!-B
		{
			fire(UserConnectionListener::BanMessage(), this, param); // !SMT!-B
		}
		else
		{
			dcdebug("Unknown $Error %s\n", param.c_str());
			fire(UserConnectionListener::ProtocolError(), this, param);
		}
	}
	else if (cmd == "Get")
	{
		x = param.find('$');
		if (x != string::npos)
		{
			fire(UserConnectionListener::Get(), this, Text::toUtf8(param.substr(0, x), m_last_encoding), Util::toInt64(param.substr(x + 1)) - (int64_t)1);
		}
	}
	else if (cmd == "Key")
	{
		if (!param.empty())
			fire(UserConnectionListener::Key(), this, param);
	}
	else if (cmd == "Lock")
	{
		if (!param.empty())
		{
			x = param.find(" Pk=");
			if (x != string::npos)
			{
				fire(UserConnectionListener::CLock(), this, param.substr(0, x), param.substr(x + 4));
			}
			else
			{
				// Workaround for faulty linux clients...
				x = param.find(' ');
				if (x != string::npos)
				{
					fire(UserConnectionListener::CLock(), this, param.substr(0, x), Util::emptyString);
				}
				else
				{
					fire(UserConnectionListener::CLock(), this, param, Util::emptyString);
				}
			}
		}
	}
	else if (cmd == "Send")
	{
		fire(UserConnectionListener::Send(), this);
	}
	else if (cmd == "MaxedOut")
	{
		fire(UserConnectionListener::MaxedOut(), this, param);
	}
	else if (cmd == "Supports")
	{
		if (!param.empty())
		{
			fire(UserConnectionListener::Supports(), this, StringTokenizer<string>(param, ' ').getTokens());
		}
	}
	else if (cmd.compare(0, 3, "ADC") == 0)
	{
		dispatch(aLine, true);
	}
	else if (cmd == "ListLen")
	{
		if (!param.empty())
		{
			fire(UserConnectionListener::ListLength(), this, param);
		}
	}
	else
	{
		if (getUser() && aLine.length() < 255)
			ClientManager::getInstance()->setUnknownCommand(getUser(), aLine);
			
		dcdebug("Unknown NMDC command: %.50s\n", aLine.c_str());
		unsetFlag(FLAG_NMDC);
	}
}

void UserConnection::connect(const string& aServer, uint16_t aPort, uint16_t localPort, BufferedSocket::NatRoles natRole)
{
	dcassert(!socket);
	
	socket = BufferedSocket::getSocket(0);
	socket->addListener(this);
#ifdef PPA_INCLUDE_SSL
	socket->connect(aServer, aPort, localPort, natRole, isSet(FLAG_SECURE), BOOLSETTING(ALLOW_UNTRUSTED_CLIENTS), true);
#else
	socket->connect(aServer, aPort, localPort, natRole, isSet(FLAG_SECURE), false, true);
#endif
}

void UserConnection::accept(const Socket& aServer)
{
	dcassert(!socket);
	socket = BufferedSocket::getSocket(0);
	socket->addListener(this);
#ifdef PPA_INCLUDE_SSL
	bool bAllowUntrusred = BOOLSETTING(ALLOW_UNTRUSTED_CLIENTS);
#else
	bool bAllowUntrusred = false;
#endif
	socket->accept(aServer, isSet(FLAG_SECURE), bAllowUntrusred);
}

void UserConnection::inf(bool withToken)
{
	AdcCommand c(AdcCommand::CMD_INF);
	c.addParam("ID", ClientManager::getInstance()->getMyCID().toBase32());
	if (withToken)
	{
		c.addParam("TO", getToken());
	}
	send(c);
}

void UserConnection::sup(const StringList& features)
{
	AdcCommand c(AdcCommand::CMD_SUP);
	for (StringIterC i = features.begin(); i != features.end(); ++i)
		c.addParam(*i);
	send(c);
}

void UserConnection::supports(const StringList& feat)
{
	string x;
	for (StringList::const_iterator i = feat.begin(); i != feat.end(); ++i)
	{
		x += *i + ' ';
	}
	send("$Supports " + x + '|');
}

void UserConnection::handle(AdcCommand::STA t, const AdcCommand& c) {
	if(c.getParameters().size() >= 2) {
		const string& code = c.getParam(0);
		if(!code.empty() && code[0] - '0' == AdcCommand::SEV_FATAL) {
			fire(UserConnectionListener::ProtocolError(), this, c.getParam(1));
			return;
		}
	}

	fire(t, this, c);
}

void UserConnection::on(Connected) noexcept
{
	lastActivity = GET_TICK();
	fire(UserConnectionListener::Connected(), this);
}

void UserConnection::on(Data, uint8_t* data, size_t len) noexcept
{
	lastActivity = GET_TICK();
	
	if (len)
	{
		const string l_nick = getUser()->getFirstNick();
		
		if (l_nick.size() > 0)
		{
			int64_t l_dic_nick = getUser()->getNickID();
			
			CViewlinkDBManager::getInstance()->store_ratio(
			    getUser()->getHubID(),
			    l_dic_nick,
			    len,
			    getRemoteIp(),
			    true,
			    l_nick);
		}
	}
	
	fire(UserConnectionListener::Data(), this, data, len);
}

void UserConnection::on(BytesSent, size_t bytes, size_t actual) noexcept
{
	lastActivity = GET_TICK();
	
	if (bytes)
	{
		const string l_nick = getUser()->getFirstNick();
		
		if (l_nick.size() > 0)
		{
			int64_t l_dic_nick = getUser()->getNickID();
			
			CViewlinkDBManager::getInstance()->store_ratio(
			    getUser()->getHubID(),
			    l_dic_nick,
			    bytes,
			    getRemoteIp(),
			    false,
			    l_nick);
		}
	}
	
	fire(UserConnectionListener::BytesSent(), this, bytes, actual);
}

void UserConnection::on(ModeChange) noexcept
{
	lastActivity = GET_TICK();
	fire(UserConnectionListener::ModeChange(), this);
}

void UserConnection::on(TransmitDone) noexcept
{
	fire(UserConnectionListener::TransmitDone(), this);
}

void UserConnection::on(Updated) noexcept
{
	fire(UserConnectionListener::Updated(), this);
}

void UserConnection::on(Failed, const string& aLine) noexcept
{
	setState(STATE_UNCONNECTED);
	fire(UserConnectionListener::Failed(), this, aLine);
	
	delete this;
}

// # ms we should aim for per segment
static const int64_t SEGMENT_TIME = 120 * 1000;
static const int64_t MIN_CHUNK_SIZE = 64 * 1024;

void UserConnection::updateChunkSize(int64_t leafSize, int64_t lastChunk, uint64_t ticks)
{

	if (chunkSize == 0)
	{
		chunkSize = std::max((int64_t)64 * 1024, std::min(lastChunk, (int64_t)1024 * 1024));
		return;
	}
	
	if (ticks <= 10)
	{
		// Can't rely on such fast transfers - double
		chunkSize *= 2;
		return;
	}
	
	double lastSpeed = (1000. * lastChunk) / ticks;
	
	int64_t targetSize = chunkSize;
	
	// How long current chunk size would take with the last speed...
	double msecs = 1000 * targetSize / lastSpeed;
	
	if (msecs < SEGMENT_TIME / 4)
	{
		targetSize *= 2;
	}
	else if (msecs < SEGMENT_TIME / 1.25)
	{
		targetSize += leafSize;
	}
	else if (msecs < SEGMENT_TIME * 1.25)
	{
		// We're close to our target size - don't change it
	}
	else if (msecs < SEGMENT_TIME * 4)
	{
		targetSize = std::max(MIN_CHUNK_SIZE, targetSize - chunkSize);
	}
	else
	{
		targetSize = std::max(MIN_CHUNK_SIZE, targetSize / 2);
	}
	
	chunkSize = targetSize;
}

void UserConnection::send(const string& aString)
{
	lastActivity = GET_TICK();
	COMMAND_DEBUG(aString, DebugManager::CLIENT_OUT, getRemoteIp());
	socket->write(aString);
}

// !SMT!-S
void UserConnection::setUser(const UserPtr& aUser)
{
	m_hintedUser.user = aUser;
	if (!socket) return;
	
	// [-]IRainman old code
	//socket->setSuperUser(false);
	
	setUploadLimit(FavoriteUser::UL_NONE);
	
	if (!aUser) return;
	
	FavoriteUser::UPLOAD_LIMIT ul = FavoriteUser::UL_NONE;
	if (!FavoriteManager::getInstance()->getUploadLimit(aUser, ul))
		return;
	
	// [-]IRainman old code
	//const FavoriteUser& u = i->second;
	//socket->setSuperUser(u.isSet(FavoriteUser::FLAG_SUPERUSER));
	
	setUploadLimit(ul);
}

void UserConnection::setUploadLimit(FavoriteUser::UPLOAD_LIMIT lim)
{
	// [!]IRainman TODO update this list!
	switch (lim)
	{
		case FavoriteUser::UL_BAN:
			disconnect(true);
			break;
		case FavoriteUser::UL_SU:
			socket->setMaxSpeed(-1);
			break;
		case FavoriteUser::UL_2:
			socket->setMaxSpeed(2 * 1024);
			break;
		case FavoriteUser::UL_5:
			socket->setMaxSpeed(5 * 1024);
			break;
		case FavoriteUser::UL_8:
			socket->setMaxSpeed(8 * 1024);
			break;
		case FavoriteUser::UL_12:
			socket->setMaxSpeed(12 * 1024);
			break;
		case FavoriteUser::UL_16:
			socket->setMaxSpeed(16 * 1024);
			break;
		case FavoriteUser::UL_24:
			socket->setMaxSpeed(24 * 1024);
			break;
		case FavoriteUser::UL_32:
			socket->setMaxSpeed(32 * 1024);
			break;
		case FavoriteUser::UL_64:
			socket->setMaxSpeed(64 * 1024);
			break;
		case FavoriteUser::UL_128:
			socket->setMaxSpeed(128 * 1024);
			break;
		case FavoriteUser::UL_256:
			socket->setMaxSpeed(256 * 1024);
			break;
		default:
			socket->setMaxSpeed(0);
	}
}

/**
 * @file
 * $Id: UserConnection.cpp 568 2011-07-24 18:28:43Z bigmuscle $
 */
